#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
                        print_function)

__license__   = 'GPL v3'
__copyright__ = '2013, Ida Leter'
__copyright__ = '2011, Jim Miller'
__docformat__ = 'restructuredtext en'

import time, os, copy, threading, re
from ConfigParser import SafeConfigParser
from StringIO import StringIO
from functools import partial
from datetime import datetime
from string import Template

from PyQt4.Qt import (QApplication, QMenu, QToolButton)

from PyQt4.Qt import QPixmap, Qt
from PyQt4.QtCore import QBuffer

from calibre.constants import numeric_version as calibre_version

from calibre.ptempfile import PersistentTemporaryFile, PersistentTemporaryDirectory, remove_dir
from calibre.ebooks.metadata import MetaInformation, authors_to_string
from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog
from calibre.gui2.dialogs.message_box import ViewLog
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.actions import InterfaceAction
from calibre.gui2 import Dispatcher

from calibre_plugins.astroph_plugin.astroph import adapters, exceptions,configurable
from calibre_plugins.astroph_plugin.astroph import base_writer as writers
from calibre_plugins.astroph_plugin.config import prefs
from calibre_plugins.astroph_plugin.dialogs import (
    DownloadDialog, LoopProgressDialog, UserPassDialog, 
    OVERWRITE, ADDNEW, SKIP, NotGoingToDownload)

class AstroPhPlugin(InterfaceAction):

    name = 'AstroPhDownloader'

    # Declare the main action associated with this plugin
    # The keyboard shortcut can be None if you dont want to use a keyboard
    # shortcut. Remember that currently calibre has no central management for
    # keyboard shortcuts, so try to use an unusual/unused shortcut.
    # (text, icon_path, tooltip, keyboard shortcut)
    # icon_path isn't in the zip--icon loaded below.
    action_spec = ('Astro-ph', None,
                   'Download scientific papers from various websites', None)

    action_type = 'global'
    
    def genesis(self):
        # This method is called once per plugin, do initial setup here

        # Set the icon for this interface action
        # The get_icons function is a builtin function defined for all your
        # plugin code. It loads icons from the plugin zip file. It returns
        # QIcon objects, if you want the actual data, use the analogous
        # get_resources builtin function.
        #
        # Note that if you are loading more than one icon, for performance, you
        # should pass a list of names to get_icons. In this case, get_icons
        # will return a dictionary mapping names to QIcons. Names that
        # are not found in the zip file will result in null QIcons.
        icon = get_icons('images/icon.png')
        
        # The qaction is automatically created from the action_spec defined
        # above
        self.qaction.setIcon(icon)
        # Call function when plugin triggered.
        self.qaction.triggered.connect(self.show_dialog)

    def show_dialog(self):
        # The base plugin object defined in __init__.py
        base_plugin_object = self.interface_action_base_plugin
        # Show the config dialog
        # The config dialog can also be shown from within
        # Preferences->Plugins, which is why the do_user_config
        # method is defined on the base plugin class
        do_user_config = base_plugin_object.do_user_config
            
        # self.gui is the main calibre GUI. It acts as the gateway to access
        # all the elements of the calibre user interface, it should also be the
        # parent of the dialog
        # DownloadDialog just collects URLs, format and presents buttons.
        d = DownloadDialog(self.gui,
                           prefs,
                           self.qaction.icon(),
                           do_user_config,  # method for config button
                           self.start_downloads, # method to start downloads
                           )
        d.show()

    def apply_settings(self):
        # No need to do anything with perfs here, but we could.
        prefs

    def start_downloads(self, url_list, options):

        #print("start_downloads:%s"%books)

        # create and pass temp dir.
        tdir = PersistentTemporaryDirectory(prefix='astroph_')
        options['tdir']=tdir
        urls=get_url_list(url_list)
        
        book_list=[]
        for url in urls:
            book={}
            book['url']=url
            book['good'] = True
            book['calibre_id'] = None
            book['title'] = 'Unknown'
            book['author_sort'] = book['author'] = ['Unknown'] # list  
            book['comment'] = ''
            book['added'] = False
            book_list.append(book)

        self.gui.status_bar.show_message(_('Started fetching metadata for %s papers.'%len(book_list)), 3000)
        
        #if 0 < len(filter(lambda x : x['good'], books)):
        LoopProgressDialog(self.gui,
                               book_list,
                               partial(self.get_metadata_for_book, options = options),
                               partial(self.start_download_list, options = options))
        # LoopProgressDialog calls get_metadata_for_book for each 'good' paper,
        # get_metadata_for_book updates book for each,
        # LoopProgressDialog calls start_download_list at the end which goes
        # into the BG, or shows list if no 'good' books.
            
    def get_metadata_for_book(self,book,options):
        '''
        Update passed in book dict with metadata from website and
        necessary data.  To be called from LoopProgressDialog
        'loop'.
        '''
        
        # The current database shown in the GUI
        # db is an instance of the class LibraryDatabase2 from database.py
        # This class has many, many methods that allow you to do a lot of
        # things.
        db = self.gui.current_db
        
        collision = options['collision']

        if not book['good']:
            # book has already been flagged bad for whatever reason.
            return
        
        url = book['url']
        print("url:%s"%url)
        
        ## was self.asphconfig, but we need to be able to change it
        ## when doing epub update.
        asphconfig = configurable.Configuration()
        adapter = adapters.getAdapter(asphconfig,url)

        # let other exceptions percolate up.
        ## two tries, that's enough if user/pass is required
        '''for x in range(0,1):
            try:
                adapter.getStoryMetadataOnly()
            except exceptions.FailedToLogin, f:
                print("Login Failed, Need Username/Password.")
                userpass = UserPassDialog(self.gui,url,f)
                userpass.exec_() # exec_ will make it act modal
                if userpass.status:
                    adapter.username = userpass.user.text()
                    adapter.password = userpass.passwd.text()'''
        
        paper = adapter.getPaperMetadataOnly()

        book['all_metadata'] = paper.getAllMetadata()
        book['title'] = paper.getMetadata("title")
        book['author_sort'] = book['author'] = paper.getList("author")
        book['publisher'] = paper.getMetadata("publisher")
        book['tags'] = paper.getList("tags")
        book['comments'] = paper.getMetadata("abstract").replace("\n"," ").replace("  "," ")
        book['arxiv'] = paper.getMetadata("arxiv")
        book['doi'] = paper.getMetadata("doi")
        book['pdfurl'] = paper.getMetadata("pdfurl")
        book['username'] = adapter.username
        book['password'] = adapter.password
        

        book['icon'] = 'plus.png'
        book['status'] = 'Add'
        book['pubdate'] = paper.getMetadataRaw('datePublished').strftime("%Y-%m-%d")
        
        book_id = None
        
        if book['calibre_id'] != None:
            # updating an existing book.  Update mode applies.
            print("update existing id:%s"%book['calibre_id'])
            book_id = book['calibre_id']
            
        # only care about collisions when not ADDNEW
        elif collision != ADDNEW:
            # 'new' book from URL.  collision handling applies.
            print("from URL(%s)"%url)

            # try to find by identifier url first.
            searchstr = 'identifiers:"=url:%s"'%url.replace(":","|")
            identicalbooks = db.search_getting_ids(searchstr, None)
            
            if (not identicalbooks) and book['arxiv']:
                searchstr = 'identifiers:"=arxiv:%s"'%paper.getMetadata("arxiv")
                print (searchstr)
                identicalbooks = db.search_getting_ids(searchstr, None)
                
                
            if len(identicalbooks) < 1:
                # find dups
                authlist = paper.getList("author")
                if len(authlist) > 100 and calibre_version < (0, 8, 61):
                    ## should be fixed from 0.8.61 on.  In the
                    ## meantime, if it matches the title *and* first
                    ## 100 authors, I'm prepared to assume it's a
                    ## match.
                    print("reduce author list to 100 only when calibre < 0.8.61")
                    authlist = authlist[:100]
                mi = MetaInformation(paper.getMetadata("title"),
                                     authlist)
                identicalbooks = db.find_identical_books(mi)
                if len(identicalbooks) > 0:
                    print("existing found by title/author(s)")

            else:
                print("existing found by identifier URL")
                
            if collision == SKIP and identicalbooks:
                raise NotGoingToDownload("Skipping duplicate paper.","list_remove.png")

            if len(identicalbooks) > 1:
                raise NotGoingToDownload("More than one identical book--can't tell which to update/overwrite.","minusminus.png")

            if len(identicalbooks)>0:
                book_id = identicalbooks.pop()
                book['calibre_id'] = book_id
                book['icon'] = 'edit-redo.png'
                book['status'] = 'Update'
            
        if book['good']: # there shouldn't be any !'good' books at this point.
            # if still 'good', make a temp file to write the output to.
            tmp = PersistentTemporaryFile(prefix='new-%s-'%book['calibre_id'],
                                               suffix='.pdf',
                                               dir=options['tdir'])
            print("title:"+book['title'])
            print("outfile:"+tmp.name)
            book['outfile'] = tmp.name            
                
        return
        
    def start_download_list(self,book_list,options):
        '''
        Called by LoopProgressDialog to start story downloads BG processing.
        adapter_list is a list of tuples of (url,adapter)
        '''
        #print("start_download_list:book_list:%s"%book_list)
        for book in book_list:
            if book['good']:
                break
        else:
            ## No good stories to try to download, go straight to
            ## list.
            info_dialog(self.gui, _('Not going to download'),
                        _('Not going to download these papers - check if they are already in your library.'),
                        show=True,
                        show_copy_button=False)
            return
        
        if book['pdfurl'] != None:    
            func = 'arbitrary_n'
            cpus = self.gui.job_manager.server.pool_size
            args = ['calibre_plugins.astroph_plugin.jobs', 'do_download_worker',
                (book_list, options, cpus)]

            desc = 'Download paper'
            job = self.gui.job_manager.run_job(
                Dispatcher(partial(self.download_list_completed,options=options)),
                func, args=args,
                description=desc)
                
        else:
            class NotJob(object):
                def __init__(self,result):
                    self.failed=False
                    self.result=result
            notjob = NotJob(book_list)
            self.download_list_completed(notjob,options=options)
            return
        
        self.gui.status_bar.show_message('Starting %d Downloads'%len(book_list),3000)


    def download_list_completed(self, job, options={}):
        if job.failed:
            self.gui.job_exception(job, dialog_title='Failed to Download Papers')
            return
        self.previous = self.gui.library_view.currentIndex()
        

        book_list = job.result
        good_list = filter(lambda x : x['good'], book_list)
        bad_list = filter(lambda x : not x['good'], book_list)
        payload = (good_list, bad_list, options)
        
        
        msg = '''
<p>Astro-ph found <b>%s</b> good and <b>%s</b> bad downloads.</p>
<p>See log for details.</p>
<p>Proceed with updating your library?</p>
'''%(len(good_list),len(bad_list))

        htmllog='<html><body><table border="1"><tr><th>Status</th><th>Title</th><th>Author</th><th>URL</th><th>Comment</th></tr>'
        for book in good_list:
            if 'status' in book:
                status = book['status']
            else:
                status = 'Good'
            htmllog = htmllog + '<tr><td>' + '</td><td>'.join([status,book['title'],", ".join(book['author']),book['url'],book['comment']]) + '</td></tr>'
        
        for book in bad_list:
            if 'status' in book:
                status = book['status']
            else:
                status = 'Bad'
            htmllog = htmllog + '<tr><td>' + '</td><td>'.join([status,book['title'],", ".join(book['author']),book['url'],book['comment']]) + '</td></tr>'
        
        htmllog = htmllog + '</table></body></html>'
        
        self.gui.proceed_question(self._do_download_list_update,
                payload, htmllog,
                'Astro-ph log', 'Astro-ph download complete', msg,
                show_copy_button=False)
    
    def _do_download_list_update(self, payload):
        
        (good_list,bad_list,options) = payload
        total_good = len(good_list)

        self.gui.status_bar.show_message(_('Adding %s papers.'%total_good))

        if total_good > 0:
            LoopProgressDialog(self.gui,
                               good_list,
                               partial(self._update_book, options=options, db=self.gui.current_db),
                               partial(self._update_books_completed, options=options),
                               init_label="Updating calibre for papers...",
                               win_title="Update calibre for papers",
                               status_prefix="Updated")
                               
    def _update_book(self,book,db=None,
                     options={'collision':ADDNEW}):
        #print("add %s %s"%(book.['title'],book['url']))
        mi = self._make_mi_from_book(book)
        self._add_or_update_book(book,options,prefs,mi)
        
    def _update_books_completed(self, book_list, options={}, showlist=True):
        
        add_list = filter(lambda x : x['good'] and x['added'], book_list)
        add_ids = [ x['calibre_id'] for x in add_list ]
        update_list = filter(lambda x : x['good'] and not x['added'], book_list)
        update_ids = [ x['calibre_id'] for x in update_list ]
        
        if len(add_list):
            ## even shows up added to searchs.  Nice.
            self.gui.library_view.model().books_added(len(add_list))
            self.gui.library_view.model().refresh_ids(add_ids)

        if len(update_list):
            self.gui.library_view.model().refresh_ids(update_ids)

        current = self.gui.library_view.currentIndex()
        self.gui.library_view.model().current_changed(current, self.previous)
        self.gui.tags_view.recount()
        
        if self.gui.cover_flow:
            self.gui.cover_flow.dataChanged()
        
        self.gui.status_bar.show_message(_('Finished Adding %d books.'%(len(update_list) + len(add_list))), 3000)
            
        if showlist and (len(update_list) + len(add_list) != len(book_list)):
        
            '''d = DisplayStoryListDialog(self.gui,
                                       'Updates completed, final status',
                                       prefs,
                                       self.qaction.icon(),
                                       book_list,
                                       label_text='Stories have be added in Calibre, some had additional problems.'
                                       )
            d.exec_()'''
    
        print("all done, remove temp dir.")
        remove_dir(options['tdir'])

        all_ids = add_ids
        all_ids.extend(update_ids)
            
    def _make_mi_from_book(self,book):
        id={'url':book['url']}
        if book.has_key('arxiv'):
            id['arXiv']=book['arxiv']
        if book.has_key('doi'):
            id['doi']=book['doi']
    
        mi = MetaInformation(book['title'],book['author']) # author is a list.
        mi.set_identifiers(id)
        mi.publisher = book['publisher']
        mi.tags = book['tags']
        mi.languages = ['en']
        mi.pubdate = book['pubdate']
        mi.comments = book['comments']
        return mi
        
    def _add_or_update_book(self,book,options,prefs,mi=None):
        db = self.gui.current_db
        
        if mi == None:
            mi = self._make_mi_from_book(book)

        book_id = book['calibre_id']
        if book_id == None:
            book_id = db.create_book_entry(mi,
                                           add_duplicates=True)
            book['calibre_id'] = book_id
            book['added'] = True
        else:
            book['added'] = False
            db.set_metadata(book_id,mi)

        
        if book['all_metadata']['authorUrl'] != None:
            authurls = book['all_metadata']['authorUrl']
            for i, auth in enumerate(book['author']):
                #print("===Update author url for %s to %s"%(auth,authurls[i]))
                autid=db.get_author_id(auth)
                db.set_link_field_for_author(autid, unicode(authurls[i]),
                                             commit=False, notify=False)
        
        if not book['pdfurl']:
            book['comment'] = "No pdf added"
        elif not db.add_format_with_hooks(book_id,
                                        'PDF',
                                        book['outfile'], index_is_id=True):
            book['comment'] = "Adding format to book failed for some reason..."
            book['good']=False
            book['icon']='dialog_error.png'
            book['status'] = 'Error'
                
        return book_id



def f(x):
    if x.strip(): return True
    else: return False

def get_url_list(urls):
    # remove dups while preserving order.
    dups=set()
    def f(x):
        x=x.strip()
        if x and x not in dups:
            dups.add(x)
            return True
        else:
            return False
    return filter(f,urls.strip().splitlines())
